module ao {

    export class  constants {
        public static keyDataContext: string = "dataContext";
    }

    export class DependencyProperty {
        private _name: string;
        private _propertyType: any;
        private _ownerType: any;
        private _propertyMetadata: PropertyMetadata;
        public static DependencyProperties: { [name: string]: DependencyProperty }

        constructor(name: string, propertyType: any, ownerType: any, propertyMetadata: PropertyMetadata) {
            this._name = name;
            this._propertyType = propertyType;
            this._ownerType = ownerType;
            this._propertyMetadata = propertyMetadata;
        }

        public get Name(): string {
            return this._name;
        }

        public get PropertyType(): any {
            return this._propertyType;
        }

        public get OwnerType(): any {
            return this._ownerType;
        }

        public get DefaultMetadata(): PropertyMetadata {
            return this._propertyMetadata;
        }

        public static Register(name: string, propertyType: any, ownerType: any, typeMetadata: PropertyMetadata, validateValueCallback: (object: any) => boolean): DependencyProperty {
            var dependencyProperty = new DependencyProperty(name, propertyType, ownerType, typeMetadata);

            DependencyProperty.DependencyProperties[name] = dependencyProperty;
            DependencyProperty.DefineDependencyProperty(name, dependencyProperty);
            DependencyProperty.DefineDPSourceElement(name);
            return dependencyProperty;
        }
 

        private static DefineDependencyProperty(propertyName: string, dp: DependencyProperty) {
            var innerProprtyName = '_' + propertyName;
            Object.defineProperty(Element.prototype, propertyName, {
                get: function () {
                    var objValue = this[innerProprtyName];
                    if (objValue === undefined && this.parentElement !== undefined && this.nodeName != "BODY") {
                        return this.parentElement[propertyName];//get defined property of parentElement
                    }
                    return this[innerProprtyName];
                },
                set: function (value) {
                    var oldValue = this[innerProprtyName];
                    this[innerProprtyName] = value;

                    var changedCallback = dp.DefaultMetadata.GetPropertyChangedCallback;
                    if (changedCallback != null) {
                        if (oldValue != value) {                           
                            var param = new DependencyPropertyChangedEventArgs(dp, oldValue, value);
                            changedCallback(this, param);
                        }
                    }
                }
            });
        }

        private static DefineDPSourceElement(propertyName: string) {

            var innerProprtyName = '_' + propertyName;

            Object.defineProperty(Element.prototype, propertyName + 'SourceElement', {
                 
                get :function() {
                    return DependencyProperty.getParentSource(innerProprtyName, this);
                }
            });
        }

        private static getParentSource(propertyName: string, element: HTMLElement): any {
            if (element[propertyName] == null && element.nodeName != "BODY") {
                return DependencyProperty.getParentSource(propertyName, element.parentElement);
            } else {
                return element;
            }
        }

    }

    export class PropertyMetadata {

        private _propertyChangedCallback: (d: any, e: DependencyPropertyChangedEventArgs) => void;
        private _coerceValueCallback: (d: any, baseValue: any) => any;

        constructor(defaultValue: any, propertyChangedCallback: (d: /*DependencyObject*/ any, e: DependencyPropertyChangedEventArgs) => void,
            coerceValueCallback: (d: /*DependencyObject*/any, baseValue: any) => any) {
            this._propertyChangedCallback = propertyChangedCallback;
            this._coerceValueCallback = coerceValueCallback;
        }

        public get GetPropertyChangedCallback(): (d: any, e: DependencyPropertyChangedEventArgs) => void {
            return this._propertyChangedCallback;
        }

        public get GetCoerceValueCallback(): (d: any, baseValue: any) => any {
            return this._coerceValueCallback;
        }
    }

    export class DependencyPropertyChangedEventArgs {
        private _property: DependencyProperty;
        private _oldValue: any;
        private _newValue: any;
        constructor(property: DependencyProperty, oldValue: any, newValue: any) {
            this._property = property;
            this._oldValue = oldValue;
            this._newValue = newValue;
        }

        public get property(): any {
            return this._property;
        }

        public get newValue(): any {
            return this._newValue;
        }

        public get oldValue(): any {
            return this._oldValue;
        }

    }

    export class BindingOperations {

        public static GetBindingExpression(target: HTMLElement/*DependencyObject*/, dp: DependencyProperty): BindingExpression {
            return null;
        }

        public static SetBinding(target: HTMLElement/*DependencyObject*/, dp: DependencyProperty, binding: BindingBase): BindingExpressionBase {
            
            var bindingExpression = new BindingExpression(target, dp, <Binding> binding); 
            
            return bindingExpression;
        }
    }

    export interface IServiceProvider {
         GetService(serviceType:any) :any;
    }

    export class HtmlServiceProvider implements  IServiceProvider {

        public TargetObject: HTMLElement;

        public TargetProperty: DependencyProperty;

        public GetService(serviceType: any): any {
            var obj = {};
            obj['TargetProperty'] = this.TargetProperty;
            obj['TargetObject'] = this.TargetObject;
            return obj; 
        }
    }

    export class BindingExpressionBase {
        private _target: HTMLElement;
        private _targetProperty: DependencyProperty;

        constructor(target: HTMLElement, targetProperty: DependencyProperty) {
            this._target = target;
            this._targetProperty = targetProperty;
        }

        public get Target(): HTMLElement {
            return this._target;
        }

        public get TargetProperty(): DependencyProperty {
            return this._targetProperty;
        }

        public UpdateSource(): void {}

        public UpdateTarget(): void {}
    }

    export class BindingExpression extends BindingExpressionBase {

        private _binding: Binding;

        constructor(target: HTMLElement, targetProperty: DependencyProperty, binding: Binding) {
            super(target, targetProperty);
            this._binding = binding;

            if (this.Target instanceof HTMLInputElement) {
                var inputElement = <HTMLInputElement> this.Target;

                var listener = (event: Event) => {
                    this.UpdateSource();
                };
                inputElement.addEventListener("blur", listener);
            }

            var targetSourceElement = this.getSourceElement();

            if (targetSourceElement != null) {
                targetSourceElement.onChangeNotify.addEvent(()=> {
                    this.UpdateTarget();
                });
            }
        }

        public get DataItem() {
            return null;
        }

        public get ParentBinding(): Binding {
            return this._binding;
        }

        public get ResolvedSource(): any {
            return null;
        }

        public get ResolvedSourcePropertyName():string {
            return null;
        }

        public UpdateSource(): void {

            var binding = this.ParentBinding;
            var source = this.getSource();
            if (source == null) {
                return;
            }
            if (this.Target instanceof HTMLInputElement) {
                var inputElement = <HTMLInputElement> this.Target;
                source[binding.Path] = inputElement.value;
            }
        }

        public UpdateTarget(): void {

            var binding = this.ParentBinding;
            var source = this.getSource();
            var value = source != null ? source[binding.Path] : '';

            if (this.Target instanceof HTMLInputElement) {
                var inputElement = <HTMLInputElement> this.Target;
                inputElement.value = value;
            } else {
                this.Target.innerHTML = value;
            }
        }

        private getSource(): any {
            var binding = this.ParentBinding;
            var source = binding.Source;
            if (source == null) {
                return  this.Target['dataContext'];
            }
            return null;
        }

        private getSourceElement(): HTMLElement {
            var binding = this.ParentBinding;
            var source = binding.Source;
            if (source == null) {
                return this.Target['dataContextSourceElement'];
            } else {
                return this.Target;
            }
        }
    }

    //-------------
    export interface IMarkupExtension {
        Name: string;
        provideValue(serviceProvider: IServiceProvider): any;
    }

    //-------------

    export class MarkupExtension implements IMarkupExtension {

        private _name: string;

        public get Name() {
            return this._name;
        }

        public set Name(value) {
            this._name = value;
        }

        public static MarkupExtensions: { [name: string]: IMarkupExtension }

        public provideValue(serviceProvider: IServiceProvider) {
            return null;
        }

        public static register(name: string, markupExtension: IMarkupExtension) {
            markupExtension.Name = name;
            MarkupExtension.MarkupExtensions[name] = markupExtension;
        }
    }

    export class BindingBase extends MarkupExtension {
        private _stringFormat: string;

        public get StringFormat() {
            return this._stringFormat;
        }

        public set StringFormat(value) {
            this._stringFormat = value;
        }

    }


    var anonymousProperty = "anonymous";

    export class Binding extends BindingBase {

        private event: Event;

        public provideValue(serviceProvider: IServiceProvider) {

            var valueProvider = serviceProvider.GetService("");

            if (valueProvider == null) {
                return null;
            }

            var bindingTarget = <HTMLElement> valueProvider['TargetObject'];
            var bindingProperty = <DependencyProperty> valueProvider['TargetProperty'];

            return BindingOperations.SetBinding(bindingTarget, bindingProperty, this);
        }

        public  Source: any ;

        public get Path(): string {
            if (this["path"] == undefined) {
                return this[anonymousProperty + "_0"];
            }
            return this["path"];
        }
    }

    export class StaticResourceExtension extends MarkupExtension {

        private _resourceKey: string;

        public get ResourceKey() {
            return this._resourceKey;
        }

        public set ResourceKey(value) {
            this._resourceKey = value;
        }

        public provideValue(serviceProvider: IServiceProvider) {
            return null;
        }
    }

    export class RelativeSourceExtension extends MarkupExtension {
        public AncestorLevel: number;
        public AncestorType: string;

        public provideValue(serviceProvider: IServiceProvider) {
            return null;
        }
    }

    export class DynamicResourceExtension extends MarkupExtension {

        private _resourceKey: string;

        public get ResourceKey() {
            return this._resourceKey;
        }
        public set ResourceKey(value) {
            this._resourceKey = value;
        }
        public provideValue(serviceProvider: IServiceProvider) {
            return null;
        }
    }

    export class HtmlMarkup {

        public static bind(): void {
            var rootElement = document.getElementsByTagName("body")[0];
            rootElement['dataContext'] = {};
            HtmlMarkup.bindInternal(rootElement);
        }

        static bindInternal(element: HTMLElement): void {

            if (element instanceof HTMLElement) {

                HtmlMarkup.applyBindingToControl(element);
                for (var i in element.children) {
                    var childElement = <HTMLElement> element.children[i];
                    HtmlMarkup.bindInternal(childElement);
                }

            }
        }

        static applyBindingToControl(element: HTMLElement): void {
            if (element == null || element.dataset == null)
                return;

            for (var key in DependencyProperty.DependencyProperties) {
                var dpDataset = element.dataset[key];

                if (dpDataset != null) {
                    var dp = DependencyProperty.DependencyProperties[key];                    

                    var markupExtensionObj = this.getMarkupExtensionObject(dpDataset);

                    if (markupExtensionObj != null) {

                        var htmlService = new HtmlServiceProvider();
                        htmlService.TargetProperty = dp;
                        htmlService.TargetObject = element;
                        var value = markupExtensionObj.provideValue(htmlService);

                        if (value instanceof BindingExpressionBase) {
                            var bindExpr = <BindingExpressionBase> value;
                            element[key + 'Expression'] = bindExpr;
                            bindExpr.UpdateTarget();
                        } else {
                            element[key] = value;
                        }
                    }
                }
            }
        }

        static getMarkupExtensionObject(markupString): IMarkupExtension {
            if (markupString != null) {
                var bindingPattern = new RegExp("{(.*)}");

                if (bindingPattern.test(markupString) == false) {
                    return null;
                }

                var extractedString = bindingPattern.exec(markupString)[1];
                var spaceAt = extractedString.indexOf(' ', 0);
                var markupExtension = extractedString.substr(0, spaceAt);

                var markupExtensionObj = Object.create(MarkupExtension.MarkupExtensions[markupExtension]);
                markupExtensionObj.constructor.apply();

                //build new object
                markupExtensionObj.name = markupExtension;
                //extract parameter string array
                var extsParams = extractedString.substr(spaceAt + 1).split(',');
                //add  parameters into markupexts object
                for (var i = 0; i < extsParams.length; i++) {

                    var splittedParams = extsParams[i].split('=');

                    if (splittedParams.length == 1) {
                        markupExtensionObj[anonymousProperty + '_' + i] = splittedParams[0];
                    }
                    else {
                        markupExtensionObj[splittedParams[0]] = splittedParams[1];
                    }
                }
                return markupExtensionObj;
            }
            return null;

        }

    }
    export interface IChangedEvent <T>{
        addEvent(listener: (sender: any, eventArg: T) => any): void;
        removeEvent(listener: (sender: any, eventArg: T) => any): void;
        dispatch(listener: (sender: any, eventArg: T) => any): void;
        clear(): void;
    }

    export class EventArg {
        
    }

    export class ChangedEvent<T>{

        private listeners: { (sender: any, eventArg: T): any }[] = [];

        constructor() {
            this.listeners = [];
        }

        addEvent(listener: (sender: any, eventArg: T) => any): void {
            if (listener == null)
                return;

            this.listeners.push(listener);
        }

        removeEvent(listener: (sender: any, eventArg: T) => any): void {
            if (listener == null || this.listeners == null)
                return;

            var index = this.listeners.indexOf(listener);
            if (index >= 0) {
                this.listeners.splice(index, 1);
            }   
        }

        dispatch(sender: any, eventArg: T): boolean {
            if (this.listeners == null)
                return false;

            var hasBeenCanceled = this.listeners.every((listener: (sender: any, eventArg: T) => any) => {
                var result = listener(sender, eventArg);
                return result !== false;
            });

            return hasBeenCanceled;
        }
        clear(): void {
            this.listeners = [];
        }
    }
}

ao.DependencyProperty.DependencyProperties = {};
ao.MarkupExtension.MarkupExtensions = {};

ao.MarkupExtension.register("binding", ao.Binding.prototype);
ao.MarkupExtension.register("staticResource", ao.StaticResourceExtension.prototype);
ao.MarkupExtension.register("dynamicResource", ao.DynamicResourceExtension.prototype);
ao.MarkupExtension.register("relativeSource", ao.RelativeSourceExtension.prototype);

//HTMLElement DataContext DP settings
interface HTMLElement {
    DataContextProperty: ao.DependencyProperty;
    setValue(dp: ao.DependencyProperty, value: any):void;
    getValue(dp: ao.DependencyProperty): any;
    clearValue(dp: ao.DependencyProperty): void;
    onChangeNotify: ao.ChangedEvent<any>;
}

interface Object {
    onChangeNotify: ao.ChangedEvent<any>;
}

HTMLElement.prototype.onChangeNotify = new ao.ChangedEvent<any>(); 

Object.prototype.onChangeNotify = new ao.ChangedEvent<any>(); 

HTMLElement.prototype.setValue = (dp: ao.DependencyProperty, value: any)=> {
    var propertyChangedCallback = dp.DefaultMetadata.GetPropertyChangedCallback;
    if (propertyChangedCallback != null) {
        var oldValue = this.getValue(dp);
        propertyChangedCallback(this, new ao.DependencyPropertyChangedEventArgs(dp, oldValue, value));
    }
    this[dp.Name] = value;
};

HTMLElement.prototype.getValue = (dp: ao.DependencyProperty)=> {
    return this[dp.Name];
};

HTMLElement.prototype.clearValue = (dp: ao.DependencyProperty) => {
    this[dp.Name] = null;
};


function DataContextPropertyChangedCallback(d: any, e: ao.DependencyPropertyChangedEventArgs) {
    var element = <HTMLInputElement>d;
    element['dataContext'] = e.newValue;
    element.onChangeNotify.dispatch(element, e.newValue);

    var changeNotify = (sender: any, evenArgs: any)=> {
        if (sender != element) {
            element.onChangeNotify.dispatch(element, e.newValue);
        }
    };

    if (e.newValue != null) {
        e.newValue.onChangeNotify.addEvent(changeNotify);
    }
    if (e.oldValue != null) {
        e.onChangeNotify.removeEvent(changeNotify);
    }
}

function onChangeNotify(sender:any, e:any) {
    
}

function DataContextCoerceValueCallback(d: any, baseValue: any) {
    var element = <HTMLElement>d;

}

var DataContextPropertyMetadata = new ao.PropertyMetadata(null, DataContextPropertyChangedCallback, DataContextCoerceValueCallback);

HTMLElement.prototype.DataContextProperty = ao.DependencyProperty.Register("dataContext", Object.prototype, HTMLElement.prototype, DataContextPropertyMetadata, null);
//---------------------

//Textbox Text DP settings
interface HTMLInputElement {
    TextProperty: ao.DependencyProperty;
}

function TextPropertyChangedCallback(d: any, e: ao.DependencyPropertyChangedEventArgs) {
    var element = <HTMLInputElement>d; 
    if (element.nodeName == "LABEL" || element.nodeName == "DIV" || element.nodeName == "SPAN") {
        element.innerHTML = e.newValue;
    } else if (element.nodeName == "INPUT") {
        element.value = e.newValue;
    }
}

function TextCoerceValueCallback(d: any, baseValue: any) {
    var inputElement = <HTMLInputElement>d;

}

var TextPropertyMetadata = new ao.PropertyMetadata("", TextPropertyChangedCallback, TextCoerceValueCallback);

HTMLInputElement.prototype.TextProperty = ao.DependencyProperty.Register("text", String.prototype, HTMLInputElement.prototype, TextPropertyMetadata, null);

//---------------------

//Apply binding onload
window.addEventListener("load", (ev: Event)=> {
    ao.HtmlMarkup.bind();
});